package com.teamide.http;

import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpCookie;
import java.nio.charset.Charset;
import java.util.List;
import java.util.zip.GZIPInputStream;

import com.teamide.util.IOUtil;

/**
 * Http响应类<br>
 * 非线程安全对象
 * 
 * @author Looly
 *
 */
public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {

	/** 持有连接对象 */
	private HttpConnection httpConnection;
	/** Http请求原始流 */
	private InputStream in;
	/** 是否异步，异步下只持有流，否则将在初始化时直接读取body内容 */
	private volatile boolean isAsync;
	/** 响应状态码 */
	private int status;
	/** 是否忽略读取Http响应体 */
	private boolean ignoreBody;

	/**
	 * 构造
	 * 
	 * @param httpConnection
	 *            {@link HttpConnection}
	 * @param charset
	 *            编码，从请求编码中获取默认编码
	 * @param isAsync
	 *            是否异步
	 * @param isIgnoreBody
	 *            是否忽略读取响应体
	 * @since 3.1.2
	 */
	protected HttpResponse(HttpConnection httpConnection, Charset charset, boolean isAsync, boolean isIgnoreBody) {

		this.httpConnection = httpConnection;
		this.charset = charset;
		this.isAsync = isAsync;
		this.ignoreBody = isIgnoreBody;
		init();
	}

	/**
	 * 获取状态码
	 * 
	 * @return 状态码
	 */
	public int getStatus() {

		return this.status;
	}

	/**
	 * 请求是否成功，判断依据为：状态码范围在200~299内。
	 * 
	 * @return 是否成功请求
	 * @since 4.1.9
	 */
	public boolean isOk() {

		return this.status >= 200 && this.status < 300;
	}

	/**
	 * 同步<br>
	 * 如果为异步状态，则暂时不读取服务器中响应的内容，而是持有Http链接的{@link InputStream}。<br>
	 * 当调用此方法时，异步状态转为同步状态，此时从Http链接流中读取body内容并暂存在内容中。如果已经是同步状态，则不进行任何操作。
	 * 
	 * @return this
	 * @throws HttpException
	 *             IO异常
	 */
	public HttpResponse sync() {

		return this.isAsync ? forceSync() : this;
	}

	// ---------------------------------------------------------------- Http
	// Response Header start
	/**
	 * 获取内容编码
	 * 
	 * @return String
	 */
	public String contentEncoding() {

		return header(Header.CONTENT_ENCODING);
	}

	/**
	 * @return 是否为gzip压缩过的内容
	 */
	public boolean isGzip() {

		final String contentEncoding = contentEncoding();
		return contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip");
	}

	/**
	 * 获取本次请求服务器返回的Cookie信息
	 * 
	 * @return Cookie字符串
	 * @since 3.1.1
	 */
	public String getCookieStr() {

		return header(Header.SET_COOKIE);
	}

	/**
	 * 获取Cookie
	 * 
	 * @return Cookie列表
	 * @since 3.1.1
	 */
	public List<HttpCookie> getCookies() {

		return HttpRequest.cookieManager.getCookieStore().getCookies();
	}

	/**
	 * 获取Cookie
	 * 
	 * @param name
	 *            Cookie名
	 * @return {@link HttpCookie}
	 * @since 4.1.4
	 */
	public HttpCookie getCookie(String name) {

		List<HttpCookie> cookie = getCookies();
		if (null != cookie) {
			for (HttpCookie httpCookie : cookie) {
				if (httpCookie.getName().equals(name)) {
					return httpCookie;
				}
			}
		}
		return null;
	}

	/**
	 * 获取Cookie值
	 * 
	 * @param name
	 *            Cookie名
	 * @return Cookie值
	 * @since 4.1.4
	 */
	public String getCookieValue(String name) {

		HttpCookie cookie = getCookie(name);
		return (null == cookie) ? null : cookie.getValue();
	}

	// ---------------------------------------------------------------- Http
	// Response Header end

	// ---------------------------------------------------------------- Body
	// start
	/**
	 * 获得服务区响应流<br>
	 * 异步模式下获取Http原生流，同步模式下获取获取到的在内存中的副本<br>
	 * 如果想在同步模式下获取流，请先调用{@link #sync()}方法强制同步<br>
	 * 流获取后处理完毕需关闭此类
	 * 
	 * @return 响应流
	 */
	public InputStream bodyStream() {

		if (isAsync) {
			return this.in;
		}
		return new ByteArrayInputStream(this.bodyBytes);
	}

	/**
	 * 获取响应流字节码<br>
	 * 此方法会转为同步模式
	 * 
	 * @return byte[]
	 */
	public byte[] bodyBytes() {

		sync();
		return this.bodyBytes;
	}

	/**
	 * 获取响应主体
	 * 
	 * @return String
	 * @throws HttpException
	 *             包装IO异常
	 */
	public String body() {

		return new String(bodyBytes(), this.charset);
	}

	// ---------------------------------------------------------------- Body end

	@Override
	public void close() {

		IOUtil.close(this.in);
		this.in = null;
		// 关闭连接
		this.httpConnection.disconnect();
	}

	// ---------------------------------------------------------------- Private
	// method start
	/**
	 * 初始化Http响应<br>
	 * 初始化包括：
	 * 
	 * <pre>
	 * 1、读取Http状态
	 * 2、读取头信息
	 * 3、持有Http流，并不关闭流
	 * </pre>
	 * 
	 * @return this
	 * @throws HttpException
	 *             IO异常
	 */
	private HttpResponse init() {

		try {
			this.status = httpConnection.responseCode();
			this.headers = httpConnection.headers();
			final Charset charset = httpConnection.getCharset();
			if (null != charset) {
				this.charset = charset;
			}

			this.in = (this.status < HttpStatus.HTTP_BAD_REQUEST) ? httpConnection.getInputStream()
					: httpConnection.getErrorStream();
		} catch (IOException e) {
			if (e instanceof FileNotFoundException) {
				// 服务器无返回内容，忽略之
			} else {
				throw new RuntimeException(e);
			}
		}
		if (null == this.in) {
			// 在一些情况下，返回的流为null，此时提供状态码说明
			this.in = new ByteArrayInputStream(("Error request, response status: " + this.status + "").getBytes());
		} else if (isGzip() && false == (in instanceof GZIPInputStream)) {
			try {
				in = new GZIPInputStream(in);
			} catch (IOException e) {
				// 在类似于Head等方法中无body返回，此时GZIPInputStream构造会出现错误，在此忽略此错误读取普通数据
				// ignore
			}
		}

		// 同步情况下强制同步
		return this.isAsync ? this : forceSync();
	}

	/**
	 * 读取主体，忽略EOFException异常
	 * 
	 * @param in
	 *            输入流
	 * @return 自身
	 * @throws IORuntimeException
	 *             IO异常
	 */
	private void readBody(InputStream in) throws IOException {

		if (ignoreBody) {
			return;
		}

		this.bodyBytes = IOUtil.read(in);
	}

	/**
	 * 强制同步，用于初始化<br>
	 * 强制同步后变化如下：
	 * 
	 * <pre>
	 * 1、读取body内容到内存
	 * 2、异步状态设为false（变为同步状态）
	 * 3、关闭Http流
	 * 4、断开与服务器连接
	 * </pre>
	 * 
	 * @return this
	 */
	private HttpResponse forceSync() {

		// 非同步状态转为同步状态
		try {
			this.readBody(this.in);
		} catch (IOException e) {
			if (e.getCause() instanceof FileNotFoundException) {
				// 服务器无返回内容，忽略之
			} else {
				throw new RuntimeException(e);
			}
		} finally {
			if (this.isAsync) {
				this.isAsync = false;
			}
			this.close();
		}
		return this;
	}

	// ---------------------------------------------------------------- Private
	// method end
}
